/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.seata.common.util;

import org.apache.seata.common.Constants;
import org.apache.seata.common.holder.ObjectHolder;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.opentest4j.AssertionFailedError;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertNull;

/**
 * The type String utils test.
 */
public class StringUtilsTest {

    private Iterator<String> emptyIterator;
    private Iterator<String> singleElementIterator;
    private Iterator<String> multipleElementsIterator;

    @BeforeEach
    void setUp() {
        emptyIterator = Collections.emptyIterator();
        singleElementIterator = Collections.singletonList("Hello").iterator();
        multipleElementsIterator = Arrays.asList("Hello", "World", "Java").iterator();
    }

    /**
     * Test is empty.
     */
    @ParameterizedTest
    @MethodSource("provideForIsNullOrEmpty")
    void testIsNullOrEmpty(String input, boolean expected) {
        assertThat(StringUtils.isNullOrEmpty(input)).isEqualTo(expected);
    }

    static Stream<Arguments> provideForIsNullOrEmpty() {
        return Stream.of(
                Arguments.of(null, true), Arguments.of("abc", false), Arguments.of("", true), Arguments.of(" ", false));
    }

    @ParameterizedTest
    @MethodSource("provideForIsBlank")
    void testIsBlank(String input, boolean expected) {
        assertThat(StringUtils.isBlank(input)).isEqualTo(expected);
    }

    static Stream<Arguments> provideForIsBlank() {
        return Stream.of(
                Arguments.of(null, true), Arguments.of("abc", false), Arguments.of("", true), Arguments.of(" ", true));
    }

    @ParameterizedTest
    @MethodSource("provideForIsNotBlank")
    void testIsNotBlank(String input, boolean expected) {
        assertThat(StringUtils.isNotBlank(input)).isEqualTo(expected);
    }

    static Stream<Arguments> provideForIsNotBlank() {
        return Stream.of(
                Arguments.of(null, false),
                Arguments.of("abc", true),
                Arguments.of("", false),
                Arguments.of(" ", false));
    }

    @Test
    public void testTrimToNull() {
        assertThat(StringUtils.trimToNull(null)).isNull();
        assertThat(StringUtils.trimToNull("abc")).isEqualTo("abc");
        assertThat(StringUtils.trimToNull("")).isNull();
        assertThat(StringUtils.trimToNull(" ")).isNull();
    }

    @Test
    public void testTrim() {
        assertThat(StringUtils.trim(null)).isNull();
        assertThat(StringUtils.trim("abc")).isEqualTo("abc");
        assertThat(StringUtils.trim("")).isEqualTo("");
        assertThat(StringUtils.trim(" ")).isEqualTo("");
    }

    @Test
    public void testIsEmpty() {
        assertThat(StringUtils.isEmpty(null)).isTrue();
        assertThat(StringUtils.isEmpty("abc")).isFalse();
        assertThat(StringUtils.isEmpty("")).isTrue();
        assertThat(StringUtils.isEmpty(" ")).isFalse();
    }

    @Test
    public void testIsNotEmpty() {
        assertThat(StringUtils.isNotEmpty(null)).isFalse();
        assertThat(StringUtils.isNotEmpty("abc")).isTrue();
        assertThat(StringUtils.isNotEmpty("")).isFalse();
        assertThat(StringUtils.isNotEmpty(" ")).isTrue();
    }

    @Test
    public void testHump2Line() {
        assertThat(StringUtils.hump2Line("abc-d").equals("abcD")).isTrue();
        assertThat(StringUtils.hump2Line("aBc").equals("a-bc")).isTrue();
        assertThat(StringUtils.hump2Line("abc").equals("abc")).isTrue();
    }

    @Test
    public void testInputStream2String() throws IOException {
        assertNull(StringUtils.inputStream2String(null));
        String data = "abc\n" + ":\"klsdf\n" + "2ks,x:\".,-3sd˚ø≤ø¬≥";
        ByteArrayInputStream inputStream = new ByteArrayInputStream(data.getBytes(Constants.DEFAULT_CHARSET));
        assertThat(StringUtils.inputStream2String(inputStream)).isEqualTo(data);
    }

    @Test
    void inputStream2Bytes() {
        assertNull(StringUtils.inputStream2Bytes(null));
        String data = "abc\n" + ":\"klsdf\n" + "2ks,x:\".,-3sd˚ø≤ø¬≥";
        byte[] bs = data.getBytes(Constants.DEFAULT_CHARSET);
        ByteArrayInputStream inputStream = new ByteArrayInputStream(data.getBytes(Constants.DEFAULT_CHARSET));
        assertThat(StringUtils.inputStream2Bytes(inputStream)).isEqualTo(bs);
    }

    @Test
    void testEquals() {
        Assertions.assertTrue(StringUtils.equals("1", "1"));
        Assertions.assertFalse(StringUtils.equals("1", "2"));
        Assertions.assertFalse(StringUtils.equals(null, "1"));
        Assertions.assertFalse(StringUtils.equals("1", null));
        Assertions.assertFalse(StringUtils.equals("", null));
        Assertions.assertFalse(StringUtils.equals(null, ""));
    }

    @Test
    void testEqualsIgnoreCase() {
        Assertions.assertTrue(StringUtils.equalsIgnoreCase("a", "a"));
        Assertions.assertTrue(StringUtils.equalsIgnoreCase("a", "A"));
        Assertions.assertTrue(StringUtils.equalsIgnoreCase("A", "a"));
        Assertions.assertFalse(StringUtils.equalsIgnoreCase("1", "2"));
        Assertions.assertFalse(StringUtils.equalsIgnoreCase(null, "1"));
        Assertions.assertFalse(StringUtils.equalsIgnoreCase("1", null));
        Assertions.assertFalse(StringUtils.equalsIgnoreCase("", null));
        Assertions.assertFalse(StringUtils.equalsIgnoreCase(null, ""));
    }

    @Test
    void testToStringAndCycleDependency() throws Exception {
        // case: String
        Assertions.assertEquals("\"aaa\"", StringUtils.toString("aaa"));

        // case: CharSequence
        Assertions.assertEquals("\"bbb\"", StringUtils.toString(new StringBuilder("bbb")));
        // case: Number
        Assertions.assertEquals("1", StringUtils.toString(1));
        // case: Boolean
        Assertions.assertEquals("true", StringUtils.toString(true));
        // case: Character
        Assertions.assertEquals("'2'", StringUtils.toString('2'));
        // case: Charset
        Assertions.assertEquals("UTF-8", StringUtils.toString(StandardCharsets.UTF_8));
        // case: Thread
        try {
            Assertions.assertEquals("Thread[main,5,main]", StringUtils.toString(Thread.currentThread()));
        } catch (AssertionFailedError e) {
            // for java21 and above
            Assertions.assertEquals(
                    "Thread[#" + Thread.currentThread().getId() + ",main,5,main]",
                    StringUtils.toString(Thread.currentThread()));
        }

        // case: Date
        Date date = new Date(2021 - 1900, 6 - 1, 15);
        Assertions.assertEquals("2021-06-15", StringUtils.toString(date));
        date.setTime(date.getTime() + 3600000);
        Assertions.assertEquals("2021-06-15 01:00", StringUtils.toString(date));
        date.setTime(date.getTime() + 60000);
        Assertions.assertEquals("2021-06-15 01:01", StringUtils.toString(date));
        date.setTime(date.getTime() + 50000);
        Assertions.assertEquals("2021-06-15 01:01:50", StringUtils.toString(date));
        date.setTime(date.getTime() + 12);
        Assertions.assertEquals("2021-06-15 01:01:50.012", StringUtils.toString(date));

        // case: Enum
        Assertions.assertEquals("ObjectHolder.INSTANCE", StringUtils.toString(ObjectHolder.INSTANCE));

        // case: Annotation
        TestAnnotation annotation = TestClass.class.getAnnotation(TestAnnotation.class);
        Assertions.assertEquals(
                "@" + TestAnnotation.class.getSimpleName() + "(test=true)", StringUtils.toString(annotation));

        // case: Class
        Class<?> clazz = TestClass.class;
        Assertions.assertEquals("Class<" + clazz.getSimpleName() + ">", StringUtils.toString(clazz));

        // case: Method
        Method method = clazz.getMethod("setObj", TestClass.class);
        Assertions.assertEquals(
                "Method<" + clazz.getSimpleName() + ".setObj(" + clazz.getSimpleName() + ")>",
                StringUtils.toString(method));

        // case: Field
        Field field = clazz.getDeclaredField("s");
        Assertions.assertEquals("Field<" + clazz.getSimpleName() + ".(String s)>", StringUtils.toString(field));

        // case: List, and cycle dependency
        List<Object> list = new ArrayList<>();
        list.add("xxx");
        list.add(111);
        list.add(list);
        Assertions.assertEquals("[\"xxx\", 111, (this ArrayList)]", StringUtils.toString(list));

        // case: String Array
        String[] strArr = new String[2];
        strArr[0] = "11";
        strArr[1] = "22";
        Assertions.assertEquals("[\"11\", \"22\"]", StringUtils.toString(strArr));
        // case: int Array
        int[] intArr = new int[2];
        intArr[0] = 11;
        intArr[1] = 22;
        Assertions.assertEquals("[11, 22]", StringUtils.toString(intArr));
        // case: Array, and cycle dependency
        Object[] array = new Object[3];
        array[0] = 1;
        array[1] = '2';
        array[2] = array;
        Assertions.assertEquals("[1, '2', (this Object[])]", StringUtils.toString(array));

        // case: Map, and cycle dependency
        Map<Object, Object> map = new HashMap<>();
        map.put("aaa", 111);
        map.put("bbb", true);
        map.put("self", map);
        Assertions.assertEquals("{\"aaa\"->111, \"bbb\"->true, \"self\"->(this HashMap)}", StringUtils.toString(map));
        Assertions.assertFalse(CycleDependencyHandler.isStarting());
        // case: Map, and cycle dependency（deep case）
        List<Object> list2 = new ArrayList<>();
        list2.add(map);
        list2.add('c');
        map.put("list", list2);
        Assertions.assertEquals(
                "{\"aaa\"->111, \"bbb\"->true, \"self\"->(this HashMap), \"list\"->[(ref HashMap), 'c']}",
                StringUtils.toString(map));
        Assertions.assertFalse(CycleDependencyHandler.isStarting());

        // case: Object
        String resultCycleDependencyA = StringUtils.toString(CycleDependency.A);
        Assertions.assertTrue(resultCycleDependencyA.startsWith("CycleDependency("));
        Assertions.assertTrue(resultCycleDependencyA.endsWith(")"));
        Assertions.assertTrue(resultCycleDependencyA.contains("s=\"a\""));
        Assertions.assertTrue(resultCycleDependencyA.contains("obj=null"));
        // case: Object, and cycle dependency
        CycleDependency obj = new CycleDependency("c");
        obj.setObj(obj);
        String resultCycleDependencyObj = StringUtils.toString(obj);
        Assertions.assertTrue(resultCycleDependencyObj.startsWith("CycleDependency("));
        Assertions.assertTrue(resultCycleDependencyObj.endsWith(")"));
        Assertions.assertTrue(resultCycleDependencyObj.contains("s=\"c\""));
        Assertions.assertTrue(resultCycleDependencyObj.contains("obj=(this CycleDependency)"));
        // case: Object
        CycleDependency obj2 = new CycleDependency("d");
        obj.setObj(obj2);
        String actualCD = StringUtils.toString(obj);
        String expectedCanonicalCD = "CycleDependency(s=\"c\", obj=CycleDependency(s=\"d\", obj=null))";
        String expectedAlternateCD = "CycleDependency(obj=CycleDependency(obj=null, s=\"d\"), s=\"c\")";
        Assertions.assertTrue(
                expectedCanonicalCD.equals(actualCD) || expectedAlternateCD.equals(actualCD),
                "Unexpected String representation for nested CycleDependency. Actual: " + actualCD);
        // case: Object, and cycle dependency
        TestClass a = new TestClass();
        a.setObj(a);
        String resultA = StringUtils.toString(a); // check for the presence of field-value pairs regardless of order
        Assertions.assertTrue(resultA.startsWith("TestClass("));
        Assertions.assertTrue(resultA.endsWith(")"));
        Assertions.assertTrue(resultA.contains("obj=(this TestClass)"));
        Assertions.assertTrue(resultA.contains("s=null"));
        // case: Object, and cycle dependency（deep case）
        TestClass b = new TestClass();
        TestClass c = new TestClass();
        b.setObj(c);
        c.setObj(a);
        a.setObj(b);
        String actual = StringUtils.toString(a);
        String expectedCanonical =
                "TestClass(obj=TestClass(obj=TestClass(obj=(ref TestClass), s=null), s=null), s=null)";
        String expectedAlternate =
                "TestClass(s=null, obj=TestClass(s=null, obj=TestClass(s=null, obj=(ref TestClass))))";
        Assertions.assertTrue(
                expectedCanonical.equals(actual) || expectedAlternate.equals(actual),
                "Unexpected String representation for deep cycle dependency. Actual: " + actual);

        // case: anonymous class from an interface
        Object anonymousObj = new TestInterface() {
            private String a = "aaa";

            @Override
            public void test() {}
        };
        Assertions.assertEquals("TestInterface$(a=\"aaa\")", StringUtils.toString(anonymousObj));

        // case: anonymous class from an abstract class
        anonymousObj = new TestAbstractClass() {
            private String a = "aaa";

            @Override
            public void test() {}
        };
        Assertions.assertEquals("TestAbstractClass$(a=\"aaa\")", StringUtils.toString(anonymousObj));

        // final confirm: do not triggered the `toString` and `hashCode` methods
        Assertions.assertFalse(TestClass.hashCodeTriggered);
        Assertions.assertFalse(TestClass.toStringTriggered);
        Assertions.assertFalse(CycleDependency.hashCodeTriggered);
        Assertions.assertFalse(CycleDependency.toStringTriggered);
    }

    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.TYPE)
    @interface TestAnnotation {
        boolean test() default false;
    }

    interface TestInterface {
        void test();
    }

    abstract class TestAbstractClass {
        abstract void test();
    }

    @TestAnnotation(test = true)
    static class TestClass {
        public static boolean hashCodeTriggered = false;
        public static boolean toStringTriggered = false;

        private TestClass obj;
        private String s;

        @Override
        public int hashCode() {
            hashCodeTriggered = true;
            return super.hashCode();
        }

        @Override
        public String toString() {
            toStringTriggered = true;
            return StringUtils.toString(this);
        }

        public TestClass getObj() {
            return obj;
        }

        public void setObj(TestClass obj) {
            this.obj = obj;
        }
    }

    static class CycleDependency {
        public static boolean hashCodeTriggered = false;
        public static boolean toStringTriggered = false;

        public static final CycleDependency A = new CycleDependency("a");
        public static final CycleDependency B = new CycleDependency("b");

        private String s;
        private CycleDependency obj;

        private CycleDependency(String s) {
            this.s = s;
        }

        public CycleDependency getObj() {
            return obj;
        }

        public void setObj(CycleDependency obj) {
            this.obj = obj;
        }

        @Override
        public int hashCode() {
            hashCodeTriggered = true;
            return super.hashCode();
        }

        @Override
        public String toString() {
            toStringTriggered = true;
            return "(" + "s=" + s + "," + "obj=" + (obj != this ? String.valueOf(obj) : "(this CycleDependency)") + ')';
        }
    }

    @Test
    void checkDataSize() {
        assertThat(StringUtils.checkDataSize("", "testdata", 10, false)).isEqualTo(Boolean.TRUE);
        assertThat(StringUtils.checkDataSize("1234567", "testdata", 17, false)).isEqualTo(Boolean.TRUE);
        assertThat(StringUtils.checkDataSize("1234567", "testdata", 4, false)).isEqualTo(Boolean.FALSE);
        Assertions.assertThrows(
                IllegalArgumentException.class, () -> StringUtils.checkDataSize("1234567", "testdata", 6, true));
        assertThat(StringUtils.checkDataSize("1234567", "testdata", 6, false)).isEqualTo(Boolean.FALSE);
    }

    @Test
    public void testHasLowerCase() {
        Assertions.assertFalse(StringUtils.hasLowerCase(null));
        Assertions.assertFalse(StringUtils.hasLowerCase("A"));
        Assertions.assertTrue(StringUtils.hasLowerCase("a"));
    }

    @Test
    public void testHasUpperCase() {
        Assertions.assertFalse(StringUtils.hasUpperCase(null));
        Assertions.assertFalse(StringUtils.hasUpperCase("a"));
        Assertions.assertTrue(StringUtils.hasUpperCase("A"));
    }

    @Test
    void joinNullIteratorReturnsNull() {
        Assertions.assertNull(StringUtils.join(null, ","));
    }

    @Test
    void joinEmptyReturnsEmptyString() {
        Assertions.assertEquals("", StringUtils.join(emptyIterator, ","));
    }

    @Test
    void joinSingleReturnsSingleElement() {
        Assertions.assertEquals("Hello", StringUtils.join(singleElementIterator, ","));
    }

    @Test
    void joinMultipleWithSeparatorReturnsSeparator() {
        Assertions.assertEquals("Hello,World,Java", StringUtils.join(multipleElementsIterator, ","));
    }

    @Test
    void joinMultipleSeparatorReturnsSeparator() {
        Assertions.assertEquals("HelloWorldJava", StringUtils.join(multipleElementsIterator, null));
    }

    @Test
    void joinMultipleAndNullReturnsJoinedString() {
        Iterator<String> mixedIterator =
                Arrays.asList("Hello", "", "World", null, "Java").iterator();
        Assertions.assertEquals("Hello,,World,,Java", StringUtils.join(mixedIterator, ","));
    }

    @Test
    void hasLengthNullCharSequenceReturnsFalse() {
        String nullCharSequence = null;
        String emptyCharSequence = "";
        String singleCharSequence = "a";
        String multipleCharSequence = "abc";
        Assertions.assertFalse(StringUtils.hasLength(nullCharSequence));
        Assertions.assertFalse(StringUtils.hasLength(emptyCharSequence));
        Assertions.assertTrue(StringUtils.hasLength(singleCharSequence));
        Assertions.assertTrue(StringUtils.hasLength(multipleCharSequence));
    }

    @Test
    void hasTextNullCharSequenceReturnsFalse() {
        String nullCharSequence = null;
        String emptyCharSequence = "";
        String singleCharSequence = "a";
        String multipleCharSequence = "abc";
        String whitespaceCharSequence = " a b c ";
        Assertions.assertFalse(StringUtils.hasText(nullCharSequence));
        Assertions.assertFalse(StringUtils.hasText(emptyCharSequence));
        Assertions.assertTrue(StringUtils.hasText(singleCharSequence));
        Assertions.assertTrue(StringUtils.hasText(multipleCharSequence));
        Assertions.assertFalse(StringUtils.hasText("   "));
        Assertions.assertTrue(StringUtils.hasText(whitespaceCharSequence));
    }

    @Test
    public void testHasLength() {
        Assertions.assertFalse(StringUtils.hasLength(null));
        Assertions.assertFalse(StringUtils.hasLength(""));
        Assertions.assertTrue(StringUtils.hasLength(" "));
        Assertions.assertTrue(StringUtils.hasLength("foo"));
    }

    @Test
    public void testHasText() {
        Assertions.assertFalse(StringUtils.hasText(null));
        Assertions.assertFalse(StringUtils.hasText(""));
        Assertions.assertFalse(StringUtils.hasText(" "));
        Assertions.assertTrue(StringUtils.hasText("foo"));
        Assertions.assertTrue(StringUtils.hasText(" foo "));
    }
}
